2.4 MuJoCo 入门教程:从零搭建四旋翼仿真环境
0. 前言
在真实四旋翼上部署和测试控制算法,通常需要较多辅助设施,如安全网、地面缓冲垫等。但对很多个人用户来说,往往不具备这些条件,因此开展四旋翼调试的危险性和成本都相对较高。例如,参数不正确就可能导致飞行器出现大幅度震荡、突然冲天等失控现象,而由于缺乏相应的保护设施,飞行器在碰撞后很容易损坏,维修也会花费大量时间。
因此,对于一种新的控制算法或一组新的参数,先在仿真环境中进行测试是更为高效合理的选择。在仿真环境中,我们已能对大部分物理特性进行模拟,飞行器在仿真中的表现与在实际部署中的特性基本能做到高度相似。仿真器让你可以在虚拟世界里随意“炸机”,炸多少次都不花钱、不伤人,也不耽误时间。当你对仿真中的控制效果满意后,再将代码部署到真机上,风险已经大大降低。
MuJoCo(Multi-Joint dynamics with Contact)是 Google DeepMind 开发的高性能物理引擎,以其速度、精度和 Python 友好性在机器人学界十分流行。本章将手把手带你从安装 MuJoCo 开始,一步步搭建一个完整的四旋翼悬停仿真环境。
读完本文后,你将能够:
- 安装 MuJoCo 并运行第一个仿真程序
- 读懂和编写 MJCF 模型文件:旋翼、传感器、执行器的 XML 定义
- 使用 Python API 加载模型、读写传感器数据、控制执行器
- 编写控制回调函数,在每步仿真中加入自己的控制逻辑
- 跑通一个完整的四旋翼仿真,为后续 SO(3)、LQR、MPC、NMPC 各章的仿真实验做好准备
目录
- MuJoCo 是什么
- 环境安装
- MJCF 模型文件:机器人的"DNA"
- 场景文件:搭建仿真舞台
- Python API:让仿真跑起来
- 回调函数:仿真过程中的"钩子"
- 驱动器与传感器:给机器人装上肌肉和感官
- 完整示例:四旋翼悬停仿真
- 常见问题与调试技巧
1. MuJoCo 是什么
MuJoCo(Multi-Joint dynamics with Contact)是 Google DeepMind 开发的开源物理引擎,专门用于机器人学、生物力学和强化学习中的高精度刚体动力学仿真。
为什么选择 MuJoCo
| 特性 | 说明 |
|---|---|
| 速度极快 | C 语言底层,支持 GPU 加速,比 PyBullet 快 3-5 倍 |
| 接触模型精确 | 凸 Gauss 原理求解约束力,比 penalty-based 方法更物理 |
| 原生 Python 绑定 | pip install mujoco 即可使用,API 设计现代 |
| MJCF 格式 | 基于 XML 的场景描述,可读性强,可版本管理 |
| 内置可视化 | 无头渲染 + 交互式 viewer,无需额外安装 OpenGL 工具包 |
| 开源免费 | Apache 2.0 协议,商业可用 |
MuJoCo 仿真管线的核心概念
┌─────────────────────────────────────────────────────────────┐
│ 仿真循环 (每步 dt) │
│ │
│ 传感器读数 ──→ 控制器计算 ──→ 驱动器写入 ──→ 物理步进 │
│ ↑ │ │
│ └──────────── 下一帧 ──────────────────────┘ │
│ │
│ MjModel (模型,只读) MjData (状态,每帧更新) │
│ · 几何体/关节/驱动器定义 · 位置/速度/加速度 │
│ · 物理参数 · 传感器数据 │
│ · 运动学树 · 接触力 │
└─────────────────────────────────────────────────────────────┘MuJoCo 将物理仿真分为两部分:
MjModel:编译后的物理模型,包含所有结构信息(刚体数量、关节参数、几何体形状等),仿真过程中不修改。MjData:动态变化的状态数据(位置、速度、加速度、接触力、传感器读数等),每步仿真都会更新。
2. 环境安装
pip install mujocoMuJoCo 自带一个基本渲染器,但如果你需要离屏渲染(如在服务器上运行),还需要 EGL 或 OSMesa:
# Linux 服务器(无显示器)
pip install mujoco[egl]
# macOS
pip install mujoco[osmesa]验证安装:
import mujoco
print(mujoco.__version__) # 应该输出 3.x.x3. MJCF 模型文件:机器人的"DNA"
MJCF(MuJoCo XML Format)使用 XML 描述机器人结构。本章通过一个四旋翼模型的真实 MJCF 文件 humprotor160.xml,逐段讲解。
3.1 根元素与仿真选项
<mujoco model="humprotor160">
<option integrator="RK4" timestep="0.002" density="1.225" viscosity="1.8e-5"/>| 属性 | 含义 | 本工程取值 | 典型范围 |
|---|---|---|---|
integrator | 数值积分方法 | RK4 | Euler / RK4 / implicit |
timestep | 仿真步长 (s) | 0.002 | 0.001 ~ 0.005 |
density | 空气密度 (kg/m³) | 1.225 | 海平面标准值 |
viscosity | 空气动力粘度 | 1.8e-5 | 常温常压空气 |
关键理解:
timestep是仿真的心脏。太大会导致仿真发散,太小则浪费计算量。对于四旋翼这种高频动态系统,0.002s 是一个较为均衡的经验值(Nyquist-Shannon 采样定理要求步长至少是系统最高频率周期的一半)。
3.2 编译器设置
<compiler inertiafromgeom="false" meshdir="assets" autolimits="true"/>| 属性 | 含义 |
|---|---|
inertiafromgeom | false = 手动指定惯量(精确),true = 从网格自动计算(近似) |
meshdir | STL/OBJ 网格文件的搜索目录 |
autolimits | 自动计算关节/驱动器的力矩范围 |
为什么惯量手动指定:对于四旋翼这种质量分布明确的结构,手动填写转动惯量矩阵比让 MuJoCo 从 STL 估算精确得多。网格自动计算假设密度均匀,而真实结构有空腔和不同材料。
3.3 默认类:DRY 原则
<default>
<default class="humprotor160">
<default class="visual">
<geom group="2" type="mesh" contype="0" conaffinity="0"/>
</default>
<default class="collision">
<geom group="3" type="mesh"/>
</default>
<site group="5"/>
</default>
</default>MuJoCo 使用 级联默认值(cascading defaults)机制来减少重复配置:
group控制可视化图层(viewer 中可切换显示/隐藏)contype=0, conaffinity=0表示visual类的几何体 不参与碰撞检测,仅用于渲染collision类的几何体用于物理碰撞
最佳实践:将可视化网格和碰撞网格分离。碰撞检测使用简化的凸包(几百个三角面),渲染使用精细网格(几万面)。这可以大幅提升仿真性能。
3.4 资源定义
<asset>
<material name="color_black" rgba="0.4 0.4 0.4 1.0"/>
<material name="color_gray" rgba="0.918 0.918 0.918 1.0"/>
<material name="color_transparent" rgba="0.918 0.918 0.918 0.25"/>
<material name="color_green" rgba="0.537 0.855 0.827 1.0"/>
<mesh file="main_collision.stl"/>
<mesh file="3DPrint.stl"/>
<mesh file="battery.stl"/>
<mesh file="motor.stl"/>
<mesh file="propeller.stl"/>
<mesh file="nylon.stl"/>
</asset><asset> 块定义可复用资源。rgba 格式为 (R, G, B, A),每个分量范围 [0, 1]。
3.5 刚体定义:Worldbody
<worldbody>
<body name="humprotor160" pos="0 0 0.3" childclass="humprotor160">
<!-- 6自由度自由运动 -->
<freejoint/>
<!-- 惯量:质心位置 | 质量(kg) | 绕三轴的转动惯量(kg·m²) -->
<inertial pos="0 0 0" mass="0.2436" diaginertia="3.58e-4 3.65e-4 6.62e-4"/>这是模型文件最核心的部分:
| 元素 | 含义 |
|---|---|
<body> | 定义刚体,"嵌套"关系表示运动学父子链 |
pos="0 0 0.3" | 初始位置:世界系 x=0, y=0, z=0.3m(离开地面一定高度) |
<freejoint/> | 自由关节 = 6 DOF(平动 3 + 转动 3),不约束任何方向 |
<inertial> | 惯性属性:pos 是相对父 body 的质心偏移,mass 是总质量,diaginertia 是主转动惯量对角线 |
如果没有
<freejoint/>,body 将被固定在世界中,像一尊雕塑。<freejoint/>使它可以自由飞行。
<!-- 可视化几何体(不参与碰撞) -->
<geom mesh="3DPrint" material="color_green" class="visual"/>
<geom mesh="battery" material="color_gray" class="visual"/>
<geom mesh="motor" material="color_gray" class="visual"/>
<geom mesh="propeller" material="color_transparent" class="visual"/>
<geom mesh="nylon" material="color_black" class="visual"/>
<!-- 碰撞几何体(不渲染,只做碰撞检测) -->
<geom mesh="main_collision" class="collision"/><geom> 是 MuJoCo 中最常见的元素。它有一个 class 属性回指之前定义的 default,用于自动继承属性。
<!-- 传感器和电机的安装点(sensors、actuators 的锚点) -->
<site name="imu"/>
<site name="motor1_site" pos=" 0.05657 0.05657 0.0"/>
<site name="motor2_site" pos=" 0.05657 -0.05657 0.0"/>
<site name="motor3_site" pos="-0.05657 -0.05657 0.0"/>
<site name="motor4_site" pos="-0.05657 0.05657 0.0"/>
<site name="center" pos="0.0 0.0 0.0"/>
</body>
</worldbody><site> 是模型上的标记点:它本身没有物理影响,但可以:
- 作为 驱动器 的施力点(推力作用在电机位置)
- 作为 传感器 的安装位置(IMU 装在机体中心)
- 在 viewer 中可视化为小球体,方便调试
pos 坐标是 相对于其父 body 原点的偏移。四个电机布置在 ±56.57mm 的 X 形对角线上(轴距 ≈ 80mm × √2)。
3.6 驱动器:给机器人装上"肌肉"
<actuator>
<!-- 四个升力电机 -->
<!-- ctrlrange: 推力范围(N) | gear: 6维齿轮比(力X,力Y,力Z,力矩X,力矩Y,力矩Z) -->
<motor ctrlrange="0 1.5" gear="0 0 1 0 0 -0.00627" site="motor1_site" name="motor1"/>
<motor ctrlrange="0 1.5" gear="0 0 1 0 0 0.00627" site="motor2_site" name="motor2"/>
<motor ctrlrange="0 1.5" gear="0 0 1 0 0 -0.00627" site="motor3_site" name="motor3"/>
<motor ctrlrange="0 1.5" gear="0 0 1 0 0 0.00627" site="motor4_site" name="motor4"/>
<!-- 外部六轴扰动(用于注入风扰等外部力和力矩) -->
<motor ctrlrange="-1.0 1.0" gear="1 0 0 0 0 0" site="center" name="forceTx"/>
<motor ctrlrange="-1.0 1.0" gear="0 1 0 0 0 0" site="center" name="forceTy"/>
<motor ctrlrange="-1.0 1.0" gear="0 0 1 0 0 0" site="center" name="forceTz"/>
<motor ctrlrange="-1.0 1.0" gear="0 0 0 1 0 0" site="center" name="torqueMx"/>
<motor ctrlrange="-1.0 1.0" gear="0 0 0 0 1 0" site="center" name="torqueMy"/>
<motor ctrlrange="-1.0 1.0" gear="0 0 0 0 0 1" site="center" name="torqueMz"/>
</actuator>MuJoCo 的 <motor> 元素充当广义力发生器。它的工作原理:
ctrl 值 × gear = 施加在 site 上的广义力 (6维: [fx, fy, fz, tx, ty, tz])以 motor1 为例:
ctrlrange="0 1.5"— 控制输入范围 0~1.5Ngear="0 0 1 0 0 -0.00627"— 只在 Z 方向产生推力,同时在 Z 轴产生一个与推力成比例的反扭矩(-0.00627的正负号区分正反桨)site="motor1_site"— 力施加在电机的安装位置
ctrlrange 有两个关键作用:限幅:控制器写入的值会被自动 clamp,归一化:可视化滑块中作为显示范围。
3.7 传感器:机器人的"感官"
<sensor>
<gyro name="sensorGyro" site="imu"/> <!-- 机体角速度 (rad/s) -->
<accelerometer name="sensorAccelerometer" site="imu"/> <!-- 机体加速度 (m/s²) -->
<framequat name="sensorFrameQuat" objtype="site" objname="imu"/> <!-- 姿态四元数 [w,x,y,z] -->
<framepos name="sensorFramePos" objtype="site" objname="imu"/> <!-- 世界位置 (m) -->
<framelinvel name="sensorFrameLinVel" objtype="site" objname="imu"/> <!-- 世界速度 (m/s) -->
</sensor>所有传感器的读数按定义顺序存储在 d.sensordata 这一个 NumPy 数组中。在代码中获取:
sensorData = d.sensordata
gyro = sensorData[0:3] # 前3个是 gyro
acc = sensorData[3:6] # 接下来3个是 accelerometer
quat = sensorData[6:10] # 4个四元数分量
pos = sensorData[10:13] # 3个位置分量
vel = sensorData[13:16] # 3个速度分量重要:
sensordata的顺序由<sensor>块中传感器的定义顺序决定,与<sensor>块中各子元素的属性名无关。
传感器类型速查表:
| 标签 | 测量量 | 维度 | 参考系 |
|---|---|---|---|
<gyro> | 角速度 | 3 | 局部(body) |
<accelerometer> | 加速度 | 3 | 局部(body) |
<framequat> | 姿态四元数 | 4 | body→world 旋转 |
<framepos> | 位置 | 3 | 世界(world) |
<framelinvel> | 线速度 | 3 | 世界(world) |
4. 场景文件:搭建仿真舞台
scene.xml 使用 <include> 引入模型定义,实现模型与场景解耦:
<mujoco model="HumpRotor160 Scene">
<include file="humprotor160.xml"/>
...这样的好处是同一个机器人模型可以被多个场景复用(室内/室外/避障场等等)。
4.1 摄像机
<camera name="world_camera" pos="0.560 -0.687 0.744"
xyaxes="0.872 0.489 -0.000 -0.260 0.465 0.846"/>pos 定义相机位置,xyaxes 是一个 3×2 旋转矩阵(去掉第三列,由前两列叉积得到),用于定义相机朝向。简单理解为相机 CS 在 world CS 下的 x 轴向量和 y 轴向量。
4.2 光照
<light pos="0 0 1.5" dir="0 0 -1" directional="true"/>directional="true"= 平行光(模拟太阳),光源位置不影响光照方向directional="false"= 点光源,从pos向四周辐射
4.3 天空盒与地面纹理
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0"
width="512" height="3072"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge"
rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
markrgb="0.8 0.8 0.8" width="300" height="300"/>
<material name="groundplane" texture="groundplane"
texuniform="true" texrepeat="5 5" reflectance="0.2"/>MuJoCo 内置了 gradient(渐变)和 checker(棋盘格)纹理,不需要 external 图片文件。
4.4 地面
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>type="plane" 创建无限大平面。其法线方向始终为 z 轴正向,即平面方程为 z = 0(未偏移时)。size 的第三个分量(0.05)将平面上移 5cm,避免与模型视觉网格发生 Z-fighting。
5. Python API:让仿真跑起来
5.1 最简示例:加载模型与步进
import mujoco
import numpy as np
# 1. 从 XML 文件加载模型
m = mujoco.MjModel.from_xml_path('humprotor160/scene.xml')
# 2. 创建数据容器
d = mujoco.MjData(m)
# 3. 仿真步进 1000 步(2秒 @ 0.002s 步长)
for i in range(1000):
mujoco.mj_step(m, d)
print(f"t={d.time:.3f}s pos=({d.qpos[0]:.3f}, {d.qpos[1]:.3f}, {d.qpos[2]:.3f})")mujoco.mj_step(m, d) 执行一步完整的物理仿真:
- 计算主动力(来自 actuator 的
ctrl) - 求解接触/约束
- 数值积分更新位置和速度
- 更新传感器数据
5.2 核心数据结构的访问
# --- 模型结构信息(M*Model,带 m 前缀,编译后只读)---
m.nq # 广义坐标维度(本工程 = 7: [x, y, z, qw, qx, qy, qz])
m.nv # 广义速度维度(本工程 = 6: [vx, vy, vz, wx, wy, wz])
m.nu # 控制输入维度(本工程 = 10: 4个电机 + 6个扰动)
m.nsensor # 传感器总数
m.opt.timestep # 仿真步长
m.body('humprotor160').id # 通过名字获取 body ID
# --- 运行时状态(M*Data,带 d 前缀,每步更新)---
d.time # 当前仿真时间
d.qpos # 广义位置 (nq,)
d.qvel # 广义速度 (nv,)
d.qacc # 广义加速度 (nv,)
d.xpos # 每个 body 的世界系位置 (nbody, 3)
d.xmat # 每个 body 的世界系旋转矩阵 (nbody, 3, 3)
d.sensordata # 所有传感器读数串联 (nsensor,)
d.actuator_force # 各 actuator 当前施加的力5.3 直接操纵状态
# 重置姿态(零速度,初始位置)
d.qpos[:] = [0, 0, 0.3, 1, 0, 0, 0] # [x, y, z, qw, qx, qy, qz]
d.qvel[:] = 0
# 设置电机推力
d.actuator('motor1').ctrl[0] = 0.5 # motor1 推力 0.5N
d.actuator('motor2').ctrl[0] = 0.5
d.actuator('motor3').ctrl[0] = 0.5
d.actuator('motor4').ctrl[0] = 0.5注意:
d.actuator('name')返回 actuator 的引用,修改它的.ctrl数组会直接影响下一步仿真。
5.4 使用 viewer 可视化
import mujoco.viewer as viewer
# 方法一:手动循环
with viewer.launch(m, d) as v:
while v.is_running():
d.actuator('motor1').ctrl[0] = 0.5
mujoco.mj_step(m, d)
v.sync() # 将 viewer 同步到当前状态
# 方法二:使用 loader 回调(像本工程一样)
viewer.launch(loader=loadCallback)viewer.launch() 会打开一个交互窗口。在窗口中你可以:
- 鼠标左键拖动:旋转视角
- 鼠标右键/滚轮:缩放
- 鼠标中键拖动:平移视角
- 双击 body:跟踪该刚体
- Ctrl+鼠标拖动:施加虚拟力扰动
6. 回调函数:仿真过程中的"钩子"
MuJoCo 支持注册控制回调函数,在每步仿真前自动调用。这是注入控制逻辑的标准方式。
6.1 注册回调
def controlCallback(m, d):
# 读取传感器数据
gyro = d.sensordata[0:3]
# 计算控制量
thrust = compute_thrust(gyro)
# 写入 actuator
d.actuator('motor1').ctrl[0] = thrust[0]
...
# 注册全局回调
mujoco.set_mjcb_control(lambda m, d: controlCallback(m, d))6.2 使用 loader 回调管理生命周期
def loadCallback(m=None, d=None):
mujoco.set_mjcb_control(None) # 先清除旧回调
m = mujoco.MjModel.from_xml_path('./humprotor160/scene.xml')
d = mujoco.MjData(m)
if m is not None:
mujoco.set_mjcb_control(lambda m, d: controlCallback(m, d))
return m, d
viewer.launch(loader=loadCallback)loader 回调的设计解决了一个关键问题:viewer 重置时如何重新创建模型。用户每次点击 viewer 的 Reset 按钮,loader 都会被调用一次,返回全新的 (m, d),保证重置是干净的。
6.3 定频控制
MuJoCo 的仿真步长可能很小(0.002s),但控制器不需要跑那么快。通过时间比较实现降频:
UPDATE_DT = 0.002 # 控制频率 = 500Hz
lastUpdateTime = 0.0
def controlCallback(m, d):
global lastUpdateTime
currentTime = d.time
# 重置检测
if currentTime < lastUpdateTime:
lastUpdateTime = currentTime
# 不到时间就不执行
if currentTime - lastUpdateTime < UPDATE_DT:
return
dt = currentTime - lastUpdateTime
lastUpdateTime = currentTime
# 控制逻辑...7. 驱动器与传感器:给机器人装上肌肉和感官
7.1 驱动器(Actuator)详解
MuJoCo 的 actuator 是广义力发生器,不仅仅局限于电机。常用类型:
| 标签 | 作用 | 用途 |
|---|---|---|
<motor> | 施加 6 维广义力 (fx, fy, fz, tx, ty, tz) | 旋翼推力、扰动力 |
<position> | 位置伺服(PD 控制) | 舵机、关节角度控制 |
<velocity> | 速度伺服 | 轮式机器人驱动 |
<cylinder> | 液压/气动缸 | 液压机器人 |
<muscle> | 肌肉模型 | 生物力学 |
gear 属性是一个 6 维向量,在 actuator 的 site 坐标系中解释:
[力X, 力Y, 力Z, 力矩X, 力矩Y, 力矩Z]通过将某些分量设为 0,可以将 actuator 的作用限制在特定方向。
7.2 传感器(Sensor)详解
| 标签 | 数据维度 | 说明 |
|---|---|---|
<gyro> | 3 | 局部系(body)角速度 |
<accelerometer> | 3 | 局部系加速度(含重力) |
<framequat> | 4 | body→world 旋转四元数 [w,x,y,z] |
<framepos> | 3 | world 位置 |
<framelinvel> | 3 | world 线速度 |
<force> | 3 | 关节/actuator 力 |
<torque> | 3 | 关节/actuator 力矩 |
<touch> | 1 | 接触检测 |
关键:
<accelerometer>测量的加速度包含重力项。如果飞行器悬停在空中,加速度计输出[0, 0, -9.81](ENU 坐标系,z 轴向上时),而不是零。这是因为加速度计实际记录的是 比力(specific force):作用在机体上所有非引力外力的合力除以质量。
8. 完整示例:四旋翼悬停仿真
将以上所有知识串联起来,下面是一个简化版悬停仿真(完整代码见 main.py):
import mujoco
import mujoco.viewer as viewer
import numpy as np
from simplemath import quat2euler, quat2mat
UPDATE_DT = 0.002
lastUpdateTime = 0.0
# 物理参数
MASS = 0.2436 # kg
HOVER_THRUST = MASS * 9.81 / 4.0 # 每电机悬停推力 ≈ 0.597N
def motorUpdate(d, dt, thrust):
"""将推力写入四个电机"""
d.actuator('motor1').ctrl[0] = thrust[0]
d.actuator('motor2').ctrl[0] = thrust[1]
d.actuator('motor3').ctrl[0] = thrust[2]
d.actuator('motor4').ctrl[0] = thrust[3]
def controlCallback(m, d):
global lastUpdateTime
currentTime = d.time
if currentTime < lastUpdateTime:
lastUpdateTime = currentTime
if currentTime - lastUpdateTime < UPDATE_DT:
return
dt = currentTime - lastUpdateTime
lastUpdateTime = currentTime
# 读取传感器数据
sd = d.sensordata
gyro = sd[0:3] # 机体角速度
acc = sd[3:6] # 机体加速度
quat = sd[6:10] # 姿态四元数 [w,x,y,z]
pos = sd[10:13] # 世界位置
vel = sd[13:16] # 世界速度
euler = quat2euler(quat)
Rwb = quat2mat(quat) # body → world 旋转
velBody = Rwb.T @ vel # world → body 线速度
# 简单的 PID 悬停控制(示意)
# 实际控制算法参考本工程后续章节
thrust = np.full(4, HOVER_THRUST)
motorUpdate(d, dt, thrust)
def loadCallback(m=None, d=None):
mujoco.set_mjcb_control(None)
m = mujoco.MjModel.from_xml_path('./humprotor160/scene.xml')
d = mujoco.MjData(m)
if m is not None:
mujoco.set_mjcb_control(lambda m, d: controlCallback(m, d))
return m, d
if __name__ == '__main__':
try:
viewer.launch(loader=loadCallback)
finally:
print("程序已退出")9. 常见问题与调试技巧
9.1 仿真发散("爆炸")
症状:几秒后位置/速度变成 NaN 或极大值。
原因与解决:
| 可能原因 | 解决方案 |
|---|---|
| 步长过大 | 减小 timestep,或用 RK4 替换 Euler |
| 控制输入过大 | 检查 ctrlrange,确保推力不超限 |
| 惯量设置不合理 | 用平行轴定理手动计算 diaginertia |
| 碰撞体穿透 | 增大 solref 刚度或 solimp 阻尼 |
9.2 传感器读数为零
检查传感器的 site 属性是否正确引用了模型中的 site 名称。site 必须在对应的 body 下定义。
9.3 可视化模型不显示
group值:viewer 默认显示 group 0-3,检查你的<geom>的group属性contype/conaffinity:这些属性不影响可见性,只影响碰撞- mesh 文件路径:
compiler meshdir指定的是相对 XML 文件所在目录的路径
9.4 调试技巧
# 打印所有 actuator 名称
for i in range(m.nu):
print(f"actuator[{i}]: {mujoco.mj_id2name(m, mujoco.mjtObj.mjOBJ_ACTUATOR, i)}")
# 打印所有 sensor 名称和维度
for i in range(m.nsensor):
name = mujoco.mj_id2name(m, mujoco.mjtObj.mjOBJ_SENSOR, i)
dim = m.sensor_dim[i]
print(f"sensor[{i}]: {name} (dim={dim})")
# 打印 sensordata 的分段索引
offset = 0
for i in range(m.nsensor):
dim = m.sensor_dim[i]
name = mujoco.mj_id2name(m, mujoco.mjtObj.mjOBJ_SENSOR, i)
print(f" {name}: sensordata[{offset}:{offset+dim}]")
offset += dim本章参考资料与引申阅读已汇总至 参考资料。
